﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace PDX.ChangeTracking
{
    /// <summary>
    /// An implementation of <see cref="ISnapshotHandler"/> that supports
    /// undo and redo functionality.
    /// </summary>
    public abstract class AbstractChangeManager<T> : INPC, ISnapshotHandler, IChangeManager where T : Snapshot
    {
        Stack<Change<T>> UndoStack { get; set; }
        Stack<Change<T>> RedoStack { get; set; }

        bool undoOrRedoInProgress = false;

        /// <summary>
        /// Initializes a new instance of the <see cref="AbstractChangeManager{T}"/> class.
        /// </summary>
        public AbstractChangeManager()
        {
            UndoStack = new Stack<Change<T>>();
            RedoStack = new Stack<Change<T>>();
        }

        Change<T> currentChange = null;
        Snapshot lastSnapshot = null;

        /// <summary>
        /// Records the specified snapshot.
        /// </summary>
        /// <param name="reported">The snapshot.</param>
        public void Handle(Snapshot reported)
        {
            lastSnapshot = reported;
            if (undoOrRedoInProgress)
            {
                return;
            }

            // let the implementor capture any custom data
            var snap = CreateCustomSnapshotAndCaptureCustomProperties(reported);

            // copy the data we know about
            snap.Editor = reported.Editor;
            snap.FocusedElement = reported.FocusedElement;
            snap.PropertyModel = reported.PropertyModel;
            snap.Value = reported.Value;


            if (currentChange == null)
            {
                // there is no current change, so start a new one
                currentChange = new Change<T>() { OriginalSnapshot = snap };
            }
            else if(!IsContinuation(snap, currentChange.OriginalSnapshot))
            {
                // this is a new change, so start a new one
                currentChange = new Change<T>() { OriginalSnapshot = snap };
            }
            else if (IsDifferent(snap, currentChange.OriginalSnapshot))
            {
                // this is an existing change, update the latest snapshot
                currentChange.LatestSnapshot = snap;
                UpdateDescription(currentChange);
            }
            else
            {
                // this is an existing change but the "subchanges"
                // have canceled each other out, resulting in a "delta-less" change
                currentChange.LatestSnapshot = null;
            }


            if (currentChange.LatestSnapshot != null &&
                currentChange != TopOfStack)
            {
                // We have a delta and the change hasn't been added yet.
                // So add it.
                UndoStack.Push(currentChange);
                RedoStack.Clear();
                Notify();
            }
            if (currentChange.LatestSnapshot == null &&
                currentChange == TopOfStack)
            {
                // We don't have a delta but the change has been added.
                // Remove it.
                UndoStack.Pop();
                Notify();
            }
        }

        Change<T> TopOfStack
        {
            get
            {
                if (UndoStack.Count < 1)
                {
                    return null;
                }
                return UndoStack.Peek();
            }
        }

        /// <summary>
        /// Creates the custom snapshot and captures any custom properties.
        /// </summary>
        /// <param name="snap">The base snapshot reported by the Editor.</param>
        /// <returns></returns>
        protected abstract T CreateCustomSnapshotAndCaptureCustomProperties(Snapshot snap);

        /// <summary>
        /// Set or updates the description of the <see cref="Change{T}"/>
        /// </summary>
        /// <param name="change">The change.</param>
        public virtual void UpdateDescription(Change<T> change)
        {
            change.Description = string.Format("{0}: from '{1}' to '{2}'",
                change.OriginalSnapshot.PropertyModel.DisplayName,
                change.OriginalSnapshot.Value,
                change.LatestSnapshot.Value);
        }

        /// <summary>
        /// Clears the Undo and Redo stacks
        /// </summary>
        public void Clear()
        {
            UndoStack.Clear();
            RedoStack.Clear();
        }

        /// <summary>
        /// Gets a value indicating whether this instance can undo.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance can undo; otherwise, <c>false</c>.
        /// </value>
        public bool CanUndo
        {
            get { return UndoStack.Count > 0; }
        }

        /// <summary>
        /// Gets a value indicating whether this instance can redo.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance can redo; otherwise, <c>false</c>.
        /// </value>
        public bool CanRedo
        {
            get { return RedoStack.Count > 0; }
        }

        /// <summary>
        /// Undoes the change at the top of the Undo stack.
        /// </summary>
        public void Undo()
        {
            if (CanUndo)
            {
                var change = UndoStack.Pop();
                try
                {
                    undoOrRedoInProgress = true;
                    UndoChange(change);
                    RedoStack.Push(change);
                    AfterUndoOrRedo();
                }
                finally
                {
                    undoOrRedoInProgress = false;
                }
            }

            Notify();
        }

        /// <summary>
        /// Redoes the change at the top of the Redo stack
        /// </summary>
        public void Redo()
        {
            if (CanRedo)
            {
                var change = RedoStack.Pop();
                try
                {
                    undoOrRedoInProgress = true;
                    RedoChange(change);
                    UndoStack.Push(change);
                    AfterUndoOrRedo();
                }
                finally
                {
                    undoOrRedoInProgress = false;
                }
            }

            Notify();
        }

        void AfterUndoOrRedo()
        {
            if (lastSnapshot != null)
            {
                var snap = CreateCustomSnapshotAndCaptureCustomProperties(lastSnapshot);
                currentChange = new Change<T>() { OriginalSnapshot = snap };
            }
            else
            {
                currentChange = null;
            }
        }

        /// <summary>
        /// Undoes the change.
        /// </summary>
        /// <param name="change">The change.</param>
        protected virtual void UndoChange(Change<T> change)
        {
            change.OriginalSnapshot.PropertyModel.Value = change.OriginalSnapshot.Value;
            change.OriginalSnapshot.FocusedElement.Focus();
        }

        /// <summary>
        /// Redoes the change.
        /// </summary>
        /// <param name="change">The change.</param>
        protected virtual void RedoChange(Change<T> change)
        {
            change.LatestSnapshot.PropertyModel.Value = change.LatestSnapshot.Value;
            change.LatestSnapshot.FocusedElement.Focus();
        }

        /// <summary>
        /// Exposes the Undoable changes for binding purposes
        /// </summary>
        public IEnumerable<Change<T>> Undoable
        {
            get { return UndoStack.Reverse().ToList(); }
        }

        /// <summary>
        /// Exposes the Redoable changes for binding purposes
        /// </summary>
        public IEnumerable<Change<T>> Redoable
        {
            get { return RedoStack.ToList(); }
        }

        void Notify()
        {
            OnPropertyChanged("Undoable");
            OnPropertyChanged("Redoable");
            OnPropertyChanged("CanUndo");
            OnPropertyChanged("CanRedo");
        }


        /// <summary>
        /// Determines whether the current snapshot is a continuation
        /// of the (potential) change designated by the original snapshot.
        /// </summary>
        /// <param name="current">The current.</param>
        /// <param name="original">The original.</param>
        /// <returns>
        ///   <c>true</c> if the specified current is a continuation; otherwise, <c>false</c>.
        /// </returns>
        protected virtual bool IsContinuation(T current, T original)
        {
            if (current.Editor != original.Editor)
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// If <see cref="IsContinuation"/> has returned true,
        /// we need to test if the value of the current snapshot differs from
        /// the value of the original snapshot. If this method returns false,
        /// the change is considered "delta-less" and is not actually a change.
        /// </summary>
        /// <param name="current">The current snapshot.</param>
        /// <param name="original">The original snapshot.</param>
        /// <returns>
        ///   <c>true</c> if the specified current snapshot is different; otherwise, <c>false</c>.
        /// </returns>
        public virtual bool IsDifferent(T current, T original)
        {
            var oldValue = original.Value;
            var newValue = current.Value;

            if (object.Equals(oldValue, newValue))
            {
                return false;
            }

            if ((oldValue == null || string.Empty.Equals(oldValue)) &&
                (newValue == null || string.Empty.Equals(newValue)))
            {
                return false;
            }

            return true;
        }
    }
}
